﻿using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace Silverstone
{
    /// <summary>
    ///     Connects two elements together to enable a property on a source element to be bound
    ///     to a property on a target element
    /// </summary>
    public class ElementNameBinder : Control, INotifyPropertyChanged
    {
        #region Properties

        public event PropertyChangedEventHandler PropertyChanged;

        public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
            "Source", typeof(string), typeof(ElementNameBinder), null);

        public static readonly DependencyProperty TargetProperty = DependencyProperty.Register(
            "Target", typeof(string), typeof(ElementNameBinder), null);

        public static readonly DependencyProperty SourcePropertyTypeProperty = DependencyProperty.Register(
            "SourcePropertyType", typeof(string), typeof(ElementNameBinder), null);

        public static readonly DependencyProperty TargetPropertyTypeProperty = DependencyProperty.Register(
            "TargetPropertyType", typeof(string), typeof(ElementNameBinder), null);

        public static readonly DependencyProperty SourcePropertyNameProperty = DependencyProperty.Register(
            "SourcePropertyName", typeof(string), typeof(ElementNameBinder), null);

        public static readonly DependencyProperty TargetPropertyNameProperty = DependencyProperty.Register(
            "TargetPropertyName", typeof(string), typeof(ElementNameBinder), null);

        private object surrogateProperty;

        public string Source
        {
            get { return (string)this.GetValue(SourceProperty); }
            set { this.SetValue(SourceProperty, value); }
        }

        public string Target
        {
            get { return (string)this.GetValue(TargetProperty); }
            set { this.SetValue(TargetProperty, value); }
        }

        public string SourcePropertyType
        {
            get { return (string)this.GetValue(SourcePropertyTypeProperty); }
            set { this.SetValue(SourcePropertyTypeProperty, value); }
        }

        public string TargetPropertyType
        {
            get { return (string)this.GetValue(TargetPropertyTypeProperty); }
            set { this.SetValue(TargetPropertyTypeProperty, value); }
        }

        public string SourcePropertyName
        {
            get { return (string)this.GetValue(SourcePropertyNameProperty); }
            set { this.SetValue(SourcePropertyNameProperty, value); }
        }

        public string TargetPropertyName
        {
            get { return (string)this.GetValue(TargetPropertyNameProperty); }
            set { this.SetValue(TargetPropertyNameProperty, value); }
        }

        public object SurrogateProperty
        {
            get { return this.surrogateProperty; }
            set
            {
                this.surrogateProperty = value;
                if (this.PropertyChanged != null)
                {
                    this.PropertyChanged(this, new PropertyChangedEventArgs("SurrogateProperty"));
                }
            }
        }

        #endregion

        public ElementNameBinder()
        {
            this.Loaded += HandleLoaded;
        }

        private void HandleLoaded(object sender, RoutedEventArgs e)
        {
            FrameworkElement source = this.FindAncestorSibling(this.Source);
            if (source == null)
            {
                throw new Exception("The source could not be found");
            }

            FrameworkElement target = this.FindAncestorSibling(this.Target);
            if (target == null)
            {
                throw new Exception("The target could not be found");
            }

            Type sourcePropertyType;
            if (string.IsNullOrEmpty(this.SourcePropertyType))
            {
                sourcePropertyType = Type.GetType(this.SourcePropertyType, true);
            }
            else
            {
                sourcePropertyType = source.GetType();
            }

            Type targetPropertyType;
            if (string.IsNullOrEmpty(this.TargetPropertyType))
            {
                targetPropertyType = Type.GetType(this.TargetPropertyType, true);
            }
            else
            {
                targetPropertyType = source.GetType();
            }

            DependencyProperty sourceProperty = (DependencyProperty) sourcePropertyType.GetField(this.SourcePropertyName + "Property").GetValue(target);
            if (sourceProperty == null)
            {
                throw new Exception("The bound property could not be found");
            }

            DependencyProperty targetProperty = (DependencyProperty) targetPropertyType.GetField(this.TargetPropertyName + "Property").GetValue(target);
            if (targetProperty == null)
            {
                throw new Exception("The bound property could not be found");
            }

            Binding targetBinding = new Binding("SurrogateProperty");
            targetBinding.Source = this;
            targetBinding.Mode = BindingMode.OneWay;
            target.SetBinding(targetProperty, targetBinding);

            Binding sourceBinding = new Binding("SurrogateProperty");
            sourceBinding.Source = this;
            sourceBinding.Mode = BindingMode.TwoWay;
            source.SetBinding(sourceProperty, sourceBinding);
        }

        private FrameworkElement FindAncestorSibling(string name)
        {
            FrameworkElement frameworkElement = this;
            while (frameworkElement != null)
            {
                object element = frameworkElement.FindName(name);

                if (element != null)
                {
                    return element as FrameworkElement;
                }

                frameworkElement = frameworkElement.Parent as FrameworkElement;
            }

            return null;
        }
    }
}